Interactive Mandelbrot Set¶

Mandelbrot Set is a fractal, which is self-similar. If you zoom in on a fractal object it will look similar, (possibly rotated), or exactly like the original shape.

There are many other known fractals .

In [1]:
import numpy as np # because arrays are defined in numpy
from numba import njit, prange  # This is the new line with numba

@njit   # this is an alias for @jit(nopython=True)
def Mand(z0, max_steps):
    z = 0j  # no need to specify type. 
    # To initialize to complex number, just assign 0j==i*0
    for itr in range(max_steps):
        if abs(z)>2:
            return itr
        z = z*z + z0
    return max_steps

@njit(parallel=True)
def Mandelbrot3(data, ext, max_steps):
    """
    ext[4]    -- array of 4 values [min_x,max_x,min_y,max_y]
    Nxy       -- int number of points in x and y direction
    max_steps -- how many steps we will try at most before we conclude the point is in the set
    """
    Nx,Ny = data.shape # 2D array should be already allocated we get its size
    for i in prange(Nx):
        for j in range(Ny):    # note that we used prange instead of range.
                                # this switches off parallelization of this loop, so that
                                # only the outside loop over i is parallelized.
            x = ext[0] + (ext[1]-ext[0])*i/(Nx-1.)
            y = ext[2] + (ext[3]-ext[2])*j/(Ny-1.)
            # creating complex number of the fly
            data[i,j] = Mand(x + y*1j, max_steps)  
# data now contains integers. 
# MandelbrotSet has value 1000, and points not in the set have value <1000.
In [3]:
%matplotlib widget  
# Tells Jupyter to use the ipympl widget backend instead of the default inline backend.
# The default backend (inline) renders static PNG images.
import matplotlib.pyplot as plt
import matplotlib
import matplotlib.cm as cm


data = np.zeros((800, 800), dtype=np.int32)
ext = np.array([-2.0, 1.0, -1.0, 1.0], dtype=np.float64)

# compile once
Mandelbrot3(data, ext, 1000)

# Interactive callbacks are always attached to a Figure canvas.
fig, ax = plt.subplots()
im = ax.imshow(-np.log(data.T + 1), extent=ext, origin="lower",
               cmap=cm.coolwarm, aspect="equal", interpolation="nearest")
# extent=ext -> Maps pixel coordinates → physical coordinates in the complex plane
# origin="lower" -> Ensures correct orientation (positive imaginary axis upward)
# aspect="equal" -> Prevents geometric distortion during zoom
# interpolation="nearest" -> Prevents smoothing (important for fractals)
#  imshow returns an image object (im)
#	This object can later be updated in place using: im.set_data(...), im.set_extent(...)

# ax stores the current view limits (xlim, ylim) that define the zoomed region.
ax.set_title("Pan/zoom, release mouse to recompute")

# Prevents multiple Mandelbrot recomputations from running at the same time.
#  Zooming triggers multiple events. 
#  Without protection callbacks can recursively call themselves
#  the notebook can freeze
#  This flag ensures only one recomputation runs at a time.
#  takeaway: Event-driven code must be re-entrancy safe.

_busy = False

def recompute_from_axes():
    global _busy, ext  # using global variables to see if something is already plotting
    if _busy:
        return
    _busy = True

    # try:
    #     # code that might fail or return early
    # finally:
    #    # code that MUST run no matter what
    
    try:
        x0, x1 = ax.get_xlim() # Reading the zoomed region from the axes
        y0, y1 = ax.get_ylim()
        # new region of the complex plane.
        ext = np.array([x0, x1, y0, y1], dtype=np.float64)
        # new calculation on the new region
        Mandelbrot3(data, ext, 1000)
        # Updating the existing image (not recreating it!)
        im.set_data(-np.log(data.T + 1)) # Replaces the image’s pixel data
        im.set_extent(ext)               # Updates the coordinate mapping
        fig.canvas.draw_idle()           # Requests a redraw when the event loop is idle.
        #  Creating a new imshow object every time would be slow and leak memory
    finally:
        _busy = False

def on_release(event):
    # only respond to releases inside our axes
    if event.inaxes is ax:
        # Ensures the callback only fires when the mouse action occurs inside the plot.
        # Prevents spurious recomputation when clicking UI elements or margins.
        recompute_from_axes()

# Mouse event hookup: 
#   Registers a callback function to be executed when the mouse button is released.
cid = fig.canvas.mpl_connect("button_release_event", on_release)
plt.show()
Figure
No description has been provided for this image
In [ ]: